/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2012-2019. All rights reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.eazegraph.lib.gesture;

import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.multimodalinput.event.TouchEvent;

/**
 * Scale gesture detector
 */
public class ScaleGestureDetector {
    /**
     * * The constant TAG
     */
    private static final String TAG = "ScaleGestureDetector";
    /**
     * * The constant TOUCH_STABILIZE_TIME
     */
    private static final long TOUCH_STABILIZE_TIME = 128; // ms
    /**
     * * The constant SCALE_FACTOR
     */
    private static final float SCALE_FACTOR = .5f;
    /**
     * * The constant ANCHORED_SCALE_MODE_NONE
     */
    private static final int ANCHORED_SCALE_MODE_NONE = 0;
    /**
     * * The constant ANCHORED_SCALE_MODE_DOUBLE_TAP
     */
    private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
    /**
     * * The constant ANCHORED_SCALE_MODE_STYLUS
     */
    private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
    /**
     * The constant M context
     */
    private final Context mContext;
    /**
     * The constant M listener
     */
    private final OnScaleGestureListener mListener;
    /**
     * The constant M handler
     */
    private final EventHandler mHandler;
    /**
     * The constant M focus x
     */
    private float mFocusX;
    /**
     * The constant M focus y
     */
    private float mFocusY;
    /**
     * The constant M quick scale enabled
     */
    private boolean mQuickScaleEnabled;
    /**
     * The constant M stylus scale enabled
     */
    private boolean mStylusScaleEnabled;
    /**
     * The constant M curr span
     */
    private float mCurrSpan;
    /**
     * The constant M prev span
     */
    private float mPrevSpan;
    /**
     * The constant M initial span
     */
    private float mInitialSpan;
    /**
     * The constant M curr span x
     */
    private float mCurrSpanX;
    /**
     * The constant M curr span y
     */
    private float mCurrSpanY;
    /**
     * The constant M prev span x
     */
    private float mPrevSpanX;
    /**
     * The constant M prev span y
     */
    private float mPrevSpanY;
    /**
     * The constant M curr time
     */
    private long mCurrTime;
    /**
     * The constant M prev time
     */
    private long mPrevTime;
    /**
     * The constant M in progress
     */
    private boolean mInProgress;
    /**
     * The constant M span slop
     */
    private int mSpanSlop;
    /**
     * The constant M min span
     */
    private int mMinSpan;
    /**
     * The constant M anchored scale start x
     */
    private float mAnchoredScaleStartX;
    /**
     * The constant M anchored scale start y
     */
    private float mAnchoredScaleStartY;
    /**
     * The constant M anchored scale mode
     */
    private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
    /**
     * The constant M gesture detector
     */
    private GestureDetector mGestureDetector;
    /**
     * The constant M event before or above starting gesture event
     */
    private boolean mEventBeforeOrAboveStartingGestureEvent;

    /**
     * Scale gesture detector
     *
     * @param context  context
     * @param listener listener
     */
    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
        this(context, listener, null);
    }

    /**
     * Scale gesture detector
     *
     * @param context  context
     * @param listener listener
     * @param handler  handler
     */
    public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
                                EventHandler handler) {
        mContext = context;
        mListener = listener;

        mSpanSlop = 16;
        mMinSpan = 468;
        mHandler = handler;

        setQuickScaleEnabled(true);

        setStylusScaleEnabled(true);

    }

    /**
     * On touch event boolean
     *
     * @param event event
     * @return the boolean
     */
    public boolean onTouchEvent(TouchEvent event) {

        mCurrTime = event.getOccurredTime();

        final int action = event.getAction();

        // Forward the event to check for double tap gesture
        if (mQuickScaleEnabled) {
            mGestureDetector.onTouchEvent(event);
        }

        final int count = event.getPointerCount();
        final boolean isStylusButtonDown =
                (event.getSourceDevice() & TouchEvent.STYLUS) != 0;

        final boolean anchoredScaleCancelled =
                mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
        final boolean streamComplete = action == TouchEvent.PRIMARY_POINT_UP ||
                action == TouchEvent.CANCEL || anchoredScaleCancelled;

        if (action == TouchEvent.PRIMARY_POINT_DOWN || streamComplete) {
            // Reset any scale in progress with the listener.
            // If it's an ACTION_DOWN we're beginning a new event stream.
            // This means the app probably didn't give us all the events. Shame on it.
            if (mInProgress) {
                mListener.onScaleEnd(this);
                mInProgress = false;
                mInitialSpan = 0;
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
            } else if (inAnchoredScaleMode() && streamComplete) {
                mInProgress = false;
                mInitialSpan = 0;
                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
            }

            if (streamComplete) {
                return true;
            }
        }

        if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
                && !streamComplete && isStylusButtonDown) {
            // Start of a button scale gesture
            mAnchoredScaleStartX = event.getPointerPosition(0).getX();
            mAnchoredScaleStartY = event.getPointerPosition(0).getY();
            mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
            mInitialSpan = 0;
        }

        final boolean configChanged = action == TouchEvent.PRIMARY_POINT_DOWN ||
                action == TouchEvent.OTHER_POINT_UP ||
                action == TouchEvent.OTHER_POINT_DOWN || anchoredScaleCancelled;

        final boolean pointerUp = action == TouchEvent.OTHER_POINT_UP;
        final int skipIndex = pointerUp ? event.getIndex() : -1;

        // Determine focal point
        float sumX = 0;
        float sumY = 0;
        final int div = pointerUp ? count - 1 : count;
        final float focusX;
        final float focusY;
        if (inAnchoredScaleMode()) {
            // In anchored scale mode, the focal pt is always where the double tap
            // or button down gesture started
            focusX = mAnchoredScaleStartX;
            focusY = mAnchoredScaleStartY;
            if (event.getPointerPosition(0).getY() < focusY) {
                mEventBeforeOrAboveStartingGestureEvent = true;
            } else {
                mEventBeforeOrAboveStartingGestureEvent = false;
            }
        } else {
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) {
                    continue;
                }
                sumX += event.getPointerPosition(i).getX();
                sumY += event.getPointerPosition(i).getY();
            }

            focusX = sumX / div;
            focusY = sumY / div;
        }

        // Determine average deviation from focal point
        float devSumX = 0;
        float devSumY = 0;
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) {
                continue;
            }

            // Convert the resulting diameter into a radius.
            devSumX += Math.abs(event.getPointerPosition(i).getX() - focusX);
            devSumY += Math.abs(event.getPointerPosition(i).getY() - focusY);
        }
        final float devX = devSumX / div;
        final float devY = devSumY / div;

        // Span is the average distance between touch points through the focal point;
        // i.e. the diameter of the circle with a radius of the average deviation from
        // the focal point.
        final float spanX = devX * 2;
        final float spanY = devY * 2;
        final float span;
        if (inAnchoredScaleMode()) {
            span = spanY;
        } else {
            span = (float) Math.hypot(spanX, spanY);
        }

        // Dispatch begin/end events as needed.
        // If the configuration changes, notify the app to reset its current state by beginning
        // a fresh scale event stream.
        final boolean wasInProgress = mInProgress;
        mFocusX = focusX;
        mFocusY = focusY;
        if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
            mListener.onScaleEnd(this);
            mInProgress = false;
            mInitialSpan = span;
        }
        if (configChanged) {
            mPrevSpanX = mCurrSpanX = spanX;
            mPrevSpanY = mCurrSpanY = spanY;
            mInitialSpan = mPrevSpan = mCurrSpan = span;
        }

        final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
        if (!mInProgress && span >= minSpan &&
                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
            mPrevSpanX = mCurrSpanX = spanX;
            mPrevSpanY = mCurrSpanY = spanY;
            mPrevSpan = mCurrSpan = span;
            mPrevTime = mCurrTime;
            mInProgress = mListener.onScaleBegin(this);
        }

        // Handle motion; focal point and span/scale factor are changing.
        if (action == TouchEvent.POINT_MOVE) {
            mCurrSpanX = spanX;
            mCurrSpanY = spanY;
            mCurrSpan = span;

            boolean updatePrev = true;

            if (mInProgress) {
                updatePrev = mListener.onScale(this);
            }

            if (updatePrev) {
                mPrevSpanX = mCurrSpanX;
                mPrevSpanY = mCurrSpanY;
                mPrevSpan = mCurrSpan;
                mPrevTime = mCurrTime;
            }
        }

        return true;
    }

    /**
     * In anchored scale mode boolean
     *
     * @return the boolean
     */
    private boolean inAnchoredScaleMode() {
        return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
    }

    /**
     * Is quick scale enabled boolean
     *
     * @return the boolean
     */
    public boolean isQuickScaleEnabled() {
        return mQuickScaleEnabled;
    }

    /**
     * Set quick scale enabled *
     *
     * @param scales scales
     */
    public void setQuickScaleEnabled(boolean scales) {
        mQuickScaleEnabled = scales;
        if (mQuickScaleEnabled && mGestureDetector == null) {
            GestureDetector.SimpleOnGestureListener gestureListener =
                    new GestureDetector.SimpleOnGestureListener() {
                        @Override
                        public boolean onDoubleTap(TouchEvent event) {
                            // Double tap: start watching for a swipe
                            mAnchoredScaleStartX = event.getPointerPosition(0).getX();
                            mAnchoredScaleStartY = event.getPointerPosition(0).getY();
                            mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
                            return true;
                        }
                    };
            mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
        }
    }

    /**
     * Is stylus scale enabled boolean
     *
     * @return the boolean
     */
    public boolean isStylusScaleEnabled() {
        return mStylusScaleEnabled;
    }

    /**
     * Set stylus scale enabled *
     *
     * @param scales scales
     */
    public void setStylusScaleEnabled(boolean scales) {
        mStylusScaleEnabled = scales;
    }

    /**
     * Is in progress boolean
     *
     * @return the boolean
     */
    public boolean isInProgress() {
        return mInProgress;
    }

    /**
     * Get focus x float
     *
     * @return the float
     */
    public float getFocusX() {
        return mFocusX;
    }

    /**
     * Get focus y float
     *
     * @return the float
     */
    public float getFocusY() {
        return mFocusY;
    }

    /**
     * Get current span float
     *
     * @return the float
     */
    public float getCurrentSpan() {
        return mCurrSpan;
    }

    /**
     * Get current span x float
     *
     * @return the float
     */
    public float getCurrentSpanX() {
        return mCurrSpanX;
    }

    /**
     * Get current span y float
     *
     * @return the float
     */
    public float getCurrentSpanY() {
        return mCurrSpanY;
    }

    /**
     * Get previous span float
     *
     * @return the float
     */
    public float getPreviousSpan() {
        return mPrevSpan;
    }

    /**
     * Get previous span x float
     *
     * @return the float
     */
    public float getPreviousSpanX() {
        return mPrevSpanX;
    }

    /**
     * Get previous span y float
     *
     * @return the float
     */
    public float getPreviousSpanY() {
        return mPrevSpanY;
    }

    /**
     * Get scale factor float
     *
     * @return the float
     */
    public float getScaleFactor() {
        if (inAnchoredScaleMode()) {
            // Drag is moving up; the further away from the gesture
            // start, the smaller the span should be, the closer,
            // the larger the span, and therefore the larger the scale
            final boolean scaleUp =
                    (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
                            (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
            final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
            return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
        }
        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    }

    /**
     * Get time delta long
     *
     * @return the long
     */
    public long getTimeDelta() {
        return mCurrTime - mPrevTime;
    }

    /**
     * Get event time long
     *
     * @return the long
     */
    public long getEventTime() {
        return mCurrTime;
    }

    /**
     * On scale gesture listener
     */
    public interface OnScaleGestureListener {

        /**
         * On scale boolean
         *
         * @param detector detector
         * @return the boolean
         */
        boolean onScale(ScaleGestureDetector detector);

        /**
         * On scale begin boolean
         *
         * @param detector detector
         * @return the boolean
         */
        boolean onScaleBegin(ScaleGestureDetector detector);

        /**
         * On scale end *
         *
         * @param detector detector
         */
        void onScaleEnd(ScaleGestureDetector detector);
    }

    /**
     * Simple on scale gesture listener
     */
    public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {

        /**
         * On scale boolean
         *
         * @param detector detector
         * @return the boolean
         */
        public boolean onScale(ScaleGestureDetector detector) {
            return false;
        }

        /**
         * On scale begin boolean
         *
         * @param detector detector
         * @return the boolean
         */
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            return true;
        }

        /**
         * On scale end *
         *
         * @param detector detector
         */
        public void onScaleEnd(ScaleGestureDetector detector) {
            // Intentionally empty
        }
    }
}
